Skip to content

fix(runtime,mcp,service-datasource): the #6504 consumer sweep — three list consumers stop claiming what a known-partial read cannot support - #8854

Merged
hotlong merged 3 commits into
mainfrom
claude/issue-6504-list-diagnosed-consumer-sweep
Aug 15, 2026
Merged

hotlong merged 3 commits into
mainfrom
claude/issue-6504-list-diagnosed-consumer-sweep

Conversation

@hotlong

@hotlong hotlong commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Part of #6504

The consumer half of #6504. PR #7721 landed IMetadataService.listDiagnosed?(type) and the two measured packages/mcp consumers; this sweeps the rest — metadata-protocol, rest, runtime and the plugins — and leaves packages/spec untouched.

The whole card is the discipline, so here is the inventory first

Every metadata-service list-family call site in the sweep's scope, qualified individually per PR #6051. Fifteen of eighteen are correct unchanged, and saying why is the deliverable, not a preamble: a caller publishing a snapshot with no count has nothing to mis-state, and "upgrade them all to listDiagnosed" would have been the wrong answer.

# Consumer Publishes a count? Class Disposition
1 service-datasource countBoundObjects ⇒ removeDatasource yes — and spends it gating + mis-describing CHANGED
2 runtime buildMcpBridge.listObjects ⇒ list_objects tool yes — totalCount mis-describing CHANGED
3 runtime external-validation-plugin.runValidation yes — "all … match", plus a count gating + mis-describing CHANGED
4 mcp stdio-data-bridge.listObjects ⇒ same tool yes, via #2's shared tool mis-describing CHANGED (the other transport of #2)
5 metadata-protocol protocol.ts getMetaItems runtime merge no — answers { type, items } non-gating unchanged, see below
6 runtime domains/meta.ts dispatcher GET /metadata/:type fallback no non-gating unchanged
7 runtime domains/mcp.ts skill listing no already handled in #8726 unchanged
8 runtime action-execution.collectActionDeclarations no — MCP gating applies downstream non-gating unchanged
9 runtime external-validation-plugin.scheduleDriftChecks no non-gating unchanged
10 plugin-security permission-evaluator.resolvePermissionSets no gating, already fail-CLOSED unchanged
11-15 bootstrap-declared-* (permissions, capabilities, positions, email templates, webhooks, sharing rules) counts of their own action only non-gating unchanged
16 plugin-hono-server /me/apps additive fallback no non-gating unchanged
17 service-datasource listDatasourceRecords no non-gating unchanged
18 service-datasource plugin.ts listObjects wiring no (its consumer #3 makes the claim) non-gating unchanged

packages/rest has no direct consumer at all. It reads metadata through protocol.getMetaItems and through packageService, never through IMetadataService's list family — verified by grep over packages/rest/src. That is a finding of the sweep, not an omission from it.

Three notes on the ones left alone, because each was a real decision:

What each changed consumer now does, and why

1. A datasource removal no longer deletes on a count it could not take

This is the sharpest consumer in the card and the reason the sweep grades above polish. removeDatasource's guard is the only thing in front of an irreversible operation that also unbinds the datasource's secret:

const bound = await countBoundObjects(name);   // derived from listObjects()
if (bound > 0) throw …                          // the whole guard
await deleteDatasourceRecord(name);
await removeSecret(existing.external.credentialsRef);

During a loader outage the object listing goes silently short, and the worst value is the benign-looking one: 0 reads exactly as nothing is bound. So the guard did not merely mis-state — it opened, and the datasource was deleted while objects were still bound to it.

It now prefers the optional countBoundObjectsDiagnosed and refuses on a degraded read, carrying the ADR-0112 envelope: SERVICE_UNAVAILABLE / 503, not this service's generic 400 — nothing about the request is wrong, the condition is a dependency outage that may clear, and the caller should retry. admin-routes.ts relays a thrown 503 envelope instead of flattening it to badRequest; the relay reads the error rather than special-casing the route, and requires both status and code so an unrelated error carrying a stray status cannot re-route itself. The loader detail rides on cause, never in the served message.

Withholding the destructive act is the plural analogue of withholding totalCount: here the "answer" is the deletion, so the only honest response is to make none. It is fully reversible in a way the deletion is not.

2 + 4. The list_objects MCP tool stops publishing totalCount on a known-partial listing

PR #7721 removed exactly this claim from objectstack://objects — the resource. The list_objects tool renders the same { objects, totalCount } from the same listing and was never covered, because the resource is served off IMetadataService directly while the tool goes through the injected McpDataBridge. A client asking how many objects does this app have therefore got an honest answer over one primitive and a confident wrong integer over the other.

Same fix, same words:

  • healthy — { objects, totalCount }, byte-identical to before. A count from a complete read is a fact this tool was always right to state.
  • degraded — the same objects, totalCount absent, and partial / returnedCount / warning / code / status: 503 in its place. A client reading totalCount gets undefined — which fails, or renders as nothing — where a plausible integer would have been believed.

returnedCount counts what is served, i.e. after the system-object filter, since naming the pre-filter number would restate the same over-claim one field along.

Both bridges implement the new optional member — stdio (packages/mcp) and HTTP (packages/runtime) — because a completeness claim must not depend on which transport a client connected over. The sentence and the 503 code moved to a shared metadata-completeness.ts rather than being copied: two surfaces answering one question must say it in one vocabulary, and mcp-server-runtime.ts imports mcp-http-tools.ts, so exporting from either would be a cycle.

3. The ADR-0015 boot gate stops announcing an all-clear over a sweep it could not complete

validateAll() sweeps listObjects(), filters to the federated objects, and the gate then logged all federated objects match their remote schema, with a count. Federated objects held by an unreadable loader were never validated, so onMismatch: 'fail' could not have fired for them — an outage silently narrows the gate and then announces a clean sweep.

Only the claim changes. The gate still validates and still refuses on every mismatch it found; on a degraded read it warns that the swept set was incomplete and names what it did validate, as a count of what was validated rather than a total.

⛔ It deliberately does not abort boot on a degraded metadata read. Turning a transient dependency outage into a refusal to start is a new failure mode bought with a diagnosis fix — the opposite of what this card is.

The verdict is asked of the metadata service directly rather than threaded through SchemaValidationReport. The plain reason: that report is declared in packages/spec, which this card does not own. The better reason: the question is about the object listing and the metadata service is where the answer lives, so routing it through a second contract would add a member every implementer must remember to fill in, to relay a fact the authority can already be asked for.

Optionality, everywhere, for one reason

countBoundObjectsDiagnosed and listObjectsDiagnosed are both optional, stacked on IMetadataService.listDiagnosed's own optionality: a host whose metadata service predates the verdict behaves exactly as it did before, and a service without it reports nothing degraded — precisely what it could express. Every changed consumer has a pin for that direction, and a second pin that absence does not become a false alarm either.

Every consumer composes the verdict the same way PR #7721 established: the items come from the resolver the call site already used, and only the verdict is asked of listDiagnosed. listObjects is its own member of IMetadataService and declares no equivalence to list('object'), so re-resolving items through the diagnosed read would presume one — the private dialect Prime Directive #12 forbids. On the implementation that ships they are the same read and share one cache entry and one single-flight slot, so the probe costs nothing.

Verification

The real loader failure is driven in packages/runtime, which depends on @objectstack/metadata: packages/runtime/src/list-diagnosed-consumer-sweep.test.ts builds a real MetadataManager whose DatabaseLoader sits over a driver whose find() throws ECONNRESET, so readListUncached()'s own catch produces the verdict and degraded is computed rather than injected. The healthy count is 3, the degraded count is 1, and both directions are asserted on the number itself.

Doubles in the other two packages, stated plainly rather than papered over — packages/mcp and packages/services/service-datasource do not depend on @objectstack/metadata, and adding that dependency for a test would be a larger change than the fix. This is the same split PR #7721 and #6055 both took. Each file's header says so, and names where the real-failure pin lives. What those files pin is what only lives there: what a datasource removal does with an untrustworthy count, and what the tool renders once it holds the verdict. The packages/mcp pin drives the real MCP HTTP transport (tools/call), not the handler in isolation, so the payload asserted is the one a client receives.

Reverse verification — four ablations, direction predicted in each test header before running:

Ablation Predicted Measured
restore the unconditional totalCount in list_objects 3 red / 3 green 3 red / 3 green
restore the plain countBoundObjects guard in removeDatasource 3 red / 5 green 4 red / 4 green, then 3 red / 5 green — see below
delete listObjectsDiagnosed from buildMcpBridge 4 red / 5 green 4 red / 5 green
restore the unconditional boot-gate all-clear 2 red / 7 green 2 red / 7 green

The failed prediction is the useful one and is recorded in the test header rather than quietly corrected. The extra red was "a complete read that finds bindings still refuses": the harness supplied that case's count only through the diagnosed member, so the ablated build read the plain count as 0 and removed the datasource. That is a fixture where the two counts disagree, which measures the wiring instead of the decision — on the shipped wiring they are the same filter over the same listing. The harness now derives the plain count from the diagnosed one, and the re-measurement matched. Ablations were applied with a patch script and reverted with git checkout HEAD -- path from the committed state; ⛔ no git stash at any point.

The mcp-tool ablation is deliberately the declared-but-unconsumed shape — the interface member and both bridge implementations left in place, only the tool's read removed — because a whole-file revert would also delete the interface member and turn the optionality cases red for the wrong reason.

Suites and gates, all run at 99dfae356 (the final commit):

  • pnpm --filter @objectstack/runtime --filter @objectstack/mcp --filter @objectstack/service-datasource test — runtime 161 files / 2425 tests passed, mcp 19 / 200, service-datasource 18 / 421. 3046 passed, 0 failed. Three new files, 23 new cases.
  • typecheck on the same three — all Done.
  • eslint on all 13 changed files — clean, exit 0.
  • Gates from node scripts/pm/dispatch-gates.mjs against the actual changed paths, all PASS: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:objectui-changeset, check:route-envelope, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check:query-options-erasure, check:type-check-coverage, check:nul-bytes (plus a grep -naP control-character self-scan over every changed file), and the judgment-call families check:error-code-casing and check:engine-double-contract — neither applies (the 503 reuses the standard catalog's SERVICE_UNAVAILABLE; no new fake engine).
  • TEST_DEBT ratchet: pnpm check:type-check-debt after a full turbo run build of the workspace — "33 ledger entries re-measured, 1926 raw tsc errors total, none above its recorded number". @objectstack/runtime measures exactly its recorded 227 and @objectstack/mcp exactly its recorded 53, so the two new hidden-layer test files contributed zero — no ledger headroom spent ([finding][devx] check:type-check-debt 的 ledger 余量会让新写的 pin 变哑:mongodb 曾有 33 条余量吞掉一次真实回退,另有 5 条目前带 4–19 余量 #6376). The one reported surplus is @objectstack/lint (-1), pre-existing and untouched by this PR. service-datasource carries no TEST_DEBT entry: its tests are inside its tsc program and typecheck clean.

One gate moved that no path derivation names, and it is worth flagging: packages/mcp/src/mcp-write-response-internal-fields.tripwire.test.ts enumerates every callable face on the stdio bridge at runtime and fails on any it has no recipe for. listObjectsDiagnosed is a new face, so the tripwire went red on first run and is now registered as the read/summary face it is — exactly the "fires on what a diff contains" class the dispatch warned about.

Scope

  • ⛔ packages/spec untouched — IMetadataService.listDiagnosed already landed in PR feat(spec,metadata,mcp): let a plural metadata read say it is known-partial (#6504) #7721, and the two new members are on package-local host-wiring interfaces.
  • ⛔ No docs/adr/**, no content/docs/releases/.
  • Changeset present (minor on runtime / mcp / service-datasource).
  • Part of #6504, not Fixes: whether the sweep is complete is the PM's call after reading the inventory above, not this PR's to assert.

Generated by Claude Code

claude added 3 commits August 15, 2026 10:36
… list consumers stop claiming what a known-partial read cannot support (#6504)

PR #7721 landed `IMetadataService.listDiagnosed?(type)`; this is the consumer
half. Every `list()`/`listObjects()` consumer in metadata-protocol, rest,
runtime and the plugins was qualified individually per PR #6051's discipline;
most publish a snapshot with no count and are correct unchanged. Three make a
claim a short read makes false:

- `removeDatasource` spent the bound-object count as the sole guard over an
  irreversible delete (which also unbinds the datasource's secret). An outage
  under-counts, and `0` reads as "nothing is bound", so the guard OPENED. It
  now refuses with SERVICE_UNAVAILABLE/503 and leaves record, secret and pool
  intact.
- the MCP `list_objects` tool published `totalCount` — the same claim PR #7721
  removed from the `objectstack://objects` resource, on the other primitive. A
  degraded read now omits the key and serves `partial`/`returnedCount`/
  `warning` plus the 503 envelope, on both bridges (stdio + HTTP).
- the ADR-0015 boot gate announced "all federated objects match" over whatever
  it could enumerate. It now warns that the sweep was incomplete and names what
  it validated. It does NOT abort boot on a degraded read.

Both new host-wiring members are optional exactly as `listDiagnosed` is.
`packages/spec` is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
…e ablation measures the decision, not the wiring (#6504)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
…ts for the bound-count pin (#6504)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
@vercel

vercel Bot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 15, 2026 10:52am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/mcp, @objectstack/runtime, @objectstack/service-datasource.

27 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/mcp)
  • content/docs/ai/agents.mdx (via @objectstack/mcp)
  • content/docs/ai/connect-mcp.mdx (via @objectstack/mcp)
  • content/docs/ai/index.mdx (via @objectstack/mcp)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/mcp)
  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/mcp, @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/environment-variables.mdx (via @objectstack/mcp)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via @objectstack/mcp, packages/runtime)
  • content/docs/permissions/system-context.mdx (via packages/mcp, packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/mcp, @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/protocol/knowledge.mdx (via @objectstack/mcp)

⛔ 2 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/mcp, @objectstack/runtime)
  • content/docs/releases/v17.mdx (via @objectstack/runtime)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 15, 2026
@hotlong
hotlong marked this pull request as ready for review August 15, 2026 11:12
@hotlong
hotlong added this pull request to the merge queue Aug 15, 2026
Merged via the queue into main with commit 20067c5 Aug 15, 2026
27 checks passed
@hotlong
hotlong deleted the claude/issue-6504-list-diagnosed-consumer-sweep branch August 15, 2026 11:29
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…commits that decided them (objectstack-ai#20609)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the first stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/services/service-messaging/src/**` and nothing else, the
largest package in the lane that no open PR or in-flight claim holds
(the claim, `5884863234`, gives the order). Later stages cover the other
packages, so this PR says `Part of` and the card stays open.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), the way the landed `packages/spec/src` stages
apply it (PR objectstack-ai#20533 is the method). That is **127 sites on 109 lines in
28 files, covering 14 numbers**: the 97 census sites outside the
generated headers, and 30 sites in test comments, which the census
defers. Each rewritten line now cites the commit in `origin/main`
history that decided what the line describes, and it says in its own
words what that commit decided.

No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/`
records the decision behind any of the 14 numbers, so every anchor is a
commit: **13 distinct shas**. No number was dropped.

Only comments changed. Every touched source file keeps its line count
(116 lines out, 116 in, over 28 files), so no line citation into these
files moves. Seven of those 116 lines held no dead citation: they are
the other half of a sentence that had to be reflowed
(`inbox-caller.ts:87`, `:88`, `messaging-service.test.ts:972`,
`notification-keyed-text-bounds.test.ts:83`,
`notification-subscription.object.ts:81`, `:82`), or a pointer that lost
its referent (`sql-outbox.ts:281`, 「the race the card describes」 to 「the
race that commit describes」, because line 278 now names the commit). No
code token moves (see the guard below).

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces (added-minus-removed over the whole
diff: 0). No PR number stands on an added line.

Fifteen dead sites are left on purpose: 12 string literals and 3
generated file headers (see the list below).

One more file: a `patch` changeset for `@objectstack/service-messaging`,
because the rewritten docblocks ship (see Changeset below).

## Census: `service-messaging`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only,
unchanged. Its surface is comment prose in `packages/**/src/**/*.ts`
with string literals blanked, and it defers `*.test.ts`. The count below
is its `allocated-but-absent` findings under
`packages/services/service-messaging/`.

| reading | tree | board | whole-repo `allocated-but-absent` |
service-messaging sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `7a1faf1a5`, run 2026-09-29T06:31:54Z to 06:35:25Z |
enumerated, 185 pages, frontier objectstack-ai#20606, 18,433 numbers | 2,457 | **100**
| 82 | 22 | 13 |
| after | head `685200760`, run 06:48:33Z to 06:52:17Z | enumerated, 185
pages, frontier objectstack-ai#20606, 18,433 numbers | 2,360 | **3** | 3 | 3 | 1 |

The before count matches the 100 that census `5884031174` read at
`f11b5f20`. The whole-repo drop is 97, exactly this diff's census sites,
and the `resolves` tally is 32,744 in both runs. The 3 left are the
generated headers below. `267c11562`, the final head, adds only the
changeset, which is outside the census surface.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes both. So a second
reading runs the gate's own exported `extractCitations` (whole-file and
comment-prose projections) and `classifyCitation` over every `.ts` file
under `service-messaging/src` (87 files), against the same enumerated
board.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `7a1faf1a5` | 613 | **142** | 100 | 30 | 3 | 9 |
| after, `685200760` | 486 | **15** | 3 | 0 | 3 | 9 |

Its src-comment column equals the census's 100, which is the control on
the second instrument. The 450 resolving citations and 21 pull-request
citations are the same in both readings.

## Per-number table

Sites and files are all dead sites in scope at the base (comments and
strings, tests included). `rewritten / left` counts comment sites
rewritten and sites left. Every anchor was read in its diff or message,
not only in its subject: it is the commit that made the change the line
describes, and its own diff or message names the number it replaces.

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#6206` | 1/1 | 1/0 | `8e13ca876`: the share-link routes pass the
whole authz envelope into enforcement instead of a four-field trim. The
line lists it as one member of the defect family behind
`assembleExecutionContext` |
| `objectstack-ai#6363` | 14/2 | 13/1 | `17d095413`: `listInbox`'s `unreadCount`
counts the total unread, not the fetched window (maintainer ruling
2026-08-07, Option A: make the declaration true); it adds
`countUnreadTotal`. The same anchor the spec stages gave this number |
| `objectstack-ai#9722` | 1/1 | 1/0 | `2074b2651`: corrects the
`sys_notification_subscription` index note — `role:` and `team:` resolve
against `sys_member` and `sys_team_member` |
| `objectstack-ai#9807` | 4/3 | 4/0 | `44738f7af`: marks the subscription-to-recipient
expansion NOT WIRED and aligns `principal` with the forms
`RecipientResolver.resolveOne()` accepts, email kept verbatim |
| `objectstack-ai#11374` | 17/6 | 16/1 | route A of the maintainer's 2026-08-24
ruling: a keyed text column declares a `maxLength` sourced from its
producer. Written as 「route A, ruling 2026-08-24」 beside `e4902d2b9`,
the commit that applied it here. `scripts/check-keyed-text-bounds.mjs`'s
header states route A in words |
| `objectstack-ai#11452` | 6/3 | 5/1 | `3b5f0360c`: the plugin-facing
`listInboxAsCaller`, scoped to the authenticated caller |
| `objectstack-ai#11453` | 26/13 | 23/3 | `1a47a5368`: `ack()` refuses a row that is
not `in_flight` (`NotificationAckError`, `DELIVERY_NOT_ELIGIBLE`), as a
compare-and-set in the SQL outbox. The same anchor stage 2 gave it |
| `objectstack-ai#11671` | 4/4 | 1/3 | `09b4f4e4e`: `os i18n extract --source-hashes`
writes the per-locale provenance companion (maintainer ruling objectstack-ai#12069
Option A, which stays cited) |
| `objectstack-ai#11741` | 6/2 | 5/1 | `b706af987`: `SendEmailInput` gains
`organizationId`, and the email channel threads it on both arms. The
same anchor stage 1 gave it |
| `objectstack-ai#11859` | 29/13 | 27/2 | `d9cf78eaa`: `ack()` takes the claimed
record back and binds its claim credential in the compare-and-set. The
same anchor stage 2 gave it |
| `objectstack-ai#12144` | 2/2 | 2/0 | `3a04b0125`: identifier ceilings are
storage-owned (`sys_metadata.name` is 255) |
| `objectstack-ai#12147` | 1/1 | 1/0 | `945e91a13`: the class-level
`check-keyed-text-bounds` gate |
| `objectstack-ai#12978` | 17/6 | 16/1 | `e4902d2b9`: declares the sourced `maxLength`
on all 15 keyed text columns of the `sys_notification_*` objects. No
commit message names the card; its diff is where every `[objectstack-ai#12978]` marker
entered the tree |
| `objectstack-ai#18424` | 14/4 | 12/2 | `879b51270`: an email or SMS channel with no
transport refuses with `transport_not_configured` instead of reporting
success |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each), and every one is an ancestor of the
base (`merge-base --is-ancestor`, exit 0 for all 13). The history was
unshallowed first (`git fetch --unshallow`, 15,062 commits), so no
anchor was read from a truncated log.

Wordings to check, each true of its commit:
- `inbox-caller.ts:86-88`: 「(objectstack-ai#6071, objectstack-ai#6551, and the share-link envelope
trim commit 8e13ca8 undid)」. `8e13ca876`'s message records the trim
(four fields kept, five dropped) and the whole-envelope fix.
- `outbox.ts:72`: 「the option-A shape commit d9cf78e's ruling
refused」. `d9cf78eaa`'s message: 「The caller never supplies an identity:
ownership is proven by round-tripping what claim() returned.」
- The fifteen `sys_notification_*` bound comments: `[commit e4902d2]
... (route A, ruling 2026-08-24)`. `e4902d2b9`'s message opens 「Every
bound names its producer in the declaration」, and `3954fb7df`'s records
the ruling's date and its A and C routes.
- `notification-keyed-text-bounds.test.ts:82-83`: the `objectstack-ai#9807` pointer
becomes 「Every other arm of the grammar commit 44738f7 documented is
narrower」, because `44738f7af` is where the email arm of the selector
grammar was written down.
- `outbox-ack-claim-ownership.integration.test.ts:40`: 「the objectstack-ai#11453 file
beside this one」 names the file itself,
`outbox-ack-precondition.integration.test.ts`.

## The 15 sites left

- **Non-test strings (3 sites, 2 lines), refusal text.** `outbox.ts:187`
(「see objectstack-ai#11453」) and `outbox.ts:210` (「(objectstack-ai#11453, objectstack-ai#11859)」) are inside
`notificationAckNotClaimedMessage` and
`notificationAckLostClaimMessage`, the messages `NotificationAckError`
carries. They are runtime strings, so they are form D, not form C. The
landed objectstack-ai#20234 stages left every string site as a token and rewrote no
refusal text, so these are left and listed, as PR objectstack-ai#20533 did. The form D
stages that did rewrite strings (objectstack-ai#20233's) cover `os migrate meta`
guidance, a different class.
- **Test titles (9 sites, 8 lines).** `describe` titles in
`email-channel.test.ts:90`, `:592`, `messaging-service.test.ts:809`,
`:1286`, `notification-keyed-text-bounds.test.ts:38` (2 numbers),
`outbox-ack-claim-ownership.integration.test.ts:109`,
`outbox-ack-precondition.integration.test.ts:110` and
`sms-channel.test.ts:90`. Tokens, left as they were.
- **Generated headers (3 sites).** Line 8 of `es-ES`, `ja-JP` and
`zh-CN` `.source-hashes.generated.ts` reads 「(objectstack-ai#11671, maintainer ruling
objectstack-ai#12069 Option A, extending objectstack-ai#8765 Option B)」. `os i18n extract` writes
that line from `packages/cli/src/utils/i18n-extract.ts:2294`, and 27
generated files across the repo carry it. A hand edit here would be
undone by the next extract, so the fix belongs at the producer in a
later stage, which regenerates every copy. The hand-written
`translations/index.ts:29` is rewritten here.

## Mechanical guard: no code token moves

The check compares leaf tokens with comments stripped, base `7a1faf1a5`
against head. It uses the TypeScript parser's leaf nodes, so template
literals are read in context, and it excludes JSDoc nodes. It ran over
all 28 touched `.ts` files.

- Real run: 52,337 base tokens, **0 files with a token change** (exit
0).
- Comment-insertion control, in `outbox.ts`: 0 files changed, as
expected (exit 0).
- Positive control, a declaration inserted into `outbox.ts`: DIFFER
(exit 1). The first attempt was a no-op: its anchor text was still
inside the replacement, so `scripts/ablation-replace.mjs` refused it
before the guard ran. It was redone with a hitting anchor.
- Positive control, one digit changed inside the kept `outbox.ts:210`
refusal string: DIFFER (exit 1).

Every mutation went through `scripts/ablation-replace.mjs`, and each
restore was proven byte-identical to the HEAD blob (`80618f8711e2`) with
`git diff HEAD` empty.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/service-messaging` is included. It says only that the
provenance comments were re-anchored.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After `pnpm --filter @objectstack/service-messaging
build`, the rewritten comments reach both halves of `dist`. `d9cf78eaa`
appears 8 times in `dist/index.d.ts`, `1a47a5368` 6 times and
`17d095413` 6 times, and `e4902d2b9` appears 15 times in
`dist/index.js`. The positive control, an unchanged
`notification-subscription.object.ts` docblock sentence, appears in
`dist/index.d.ts`, and a negative control phrase appears nowhere. The
only dead numbers left in `dist` are the two kept refusal strings.

## Gates (head `267c11562`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
(self-test, 114 cases in 8 batteries) exits 0, and `node
scripts/check-issue-citations.mjs` exits 0. The diff-scoped run judged 5
citations across 19 files, and all 5 resolve.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `267c11562` derived 64 families.
They include all 50 derived at dispatch, plus 14 more. All 64 exit 0.
`--ran` reports 64 run, 0 NOT MEASURED, 0 unrun, and exits 0.
- Three gates first exited 3 (PREREQUISITE NOT MET) because the
workspace was unbuilt: `check:dual-build-cjs-loads`, `check:i18n` and
`check:type-check-debt`. A full `turbo run build` of `./packages/*` and
`./packages/*/*` then ran under the shared verify lock (71 tasks, exit
0). The first two exited 0 on their rerun.
- `check:type-check-debt` exited 3 once more: `outbox.ts`'s mtime had
moved during the guard controls, although its bytes had not, so turbo's
cache hit left `dist` older than the source. A direct `pnpm --filter
@objectstack/service-messaging build` then let it exit 0 (4 ledger
entries re-measured, none above its number).
- **Tests and typecheck:**
- `pnpm --filter @objectstack/service-messaging test`: 46 files and 507
tests pass, covering every touched test file.
- `pnpm --filter @objectstack/service-messaging typecheck` exits 0. Its
`tsc` program lists all 46 test files and 87 files under `src/` in total
(`--listFiles`).
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 28 touched `.ts` files gives 28 files, 0 errors and 0
warnings. All 28 are in eslint's own population (`isPathIgnored` is
false for each). `eslint.config.mjs` never enables type-aware linting
(no `parserOptions.project`, as its own line 328 states), so a comment
edit here cannot move the verdict on any untouched file. The repo-wide
`pnpm lint` is CI's run.
- **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of
the 28 files for control bytes finds none.

## Acceptance notes

- **The census instrument returned a truncated board once, at exit 0.**
The first `--census --json` run of this stage (06:25:55Z, base
`7a1faf1a5`) read `enumerated (85 pages)`, frontier objectstack-ai#8854, 8,444
numbers, when the newest number was above objectstack-ai#20600. The `Link` header of
its 85th page had carried no `rel="next"`, so `enumerateBoard` stopped
and classified 16,187 citations as `never-issued`. A reader counting
only `allocated-but-absent`, as this stage's count does, would have got
8 service-messaging sites instead of 100, silently. The next four
enumerations in this session read 185 pages and frontier objectstack-ai#20606, and the
counts above come from those. Nothing in `enumerateBoard` compares its
frontier with the newest issue number, which `probeBoard` does read.
Reported to the seat, not changed here: this stage makes no instrument
change.
- **What stays for later stages.**
- The 3 generated `objectstack-ai#11671` headers, whose producer is
`packages/cli/src/utils/i18n-extract.ts:2294`. That line is the
repo-wide carrier (27 generated files).
- The 3 refusal-string sites in `outbox.ts` (form D) and the 9
test-title sites.
- **Base.** The branch is 9 commits behind `origin/main` (`0f6dcac5e`,
read at 07:23Z). One of them, `8c87d26a5` (the version packages
release), touches `service-messaging`, but only its `CHANGELOG.md` and
`package.json`, and neither is in this diff. So there was no merge.
- **Anchors shared with the spec stages.** `17d095413` (objectstack-ai#6363),
`1a47a5368` (objectstack-ai#11453), `d9cf78eaa` (objectstack-ai#11859) and `b706af987` (objectstack-ai#11741) are
the anchors stages 1 and 2 already gave the same numbers in
`packages/spec/src`, so each number carries one anchor across the tree.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants